package com.xz.simplerpc.extension;

import com.xz.simplerpc.utils.Holder;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * create by fingal by 2018/01/06
 */
public class ExtensionLoader<T> {
    private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);

    private static final String SERVICES_DIRECTORY = "META-INF/services/";

    private static final Pattern pattern = Pattern.compile("\\s*([A-Za-z0-9&._]+)\\s*=\\s*([A-Za-z0-9&_.]+)");

    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();

    private Map<String, Class<T>> classes = new HashMap<>();

    private final Holder<Object> cachedAdaptiveInstance = new Holder<Object>();

    private Class<?> type;

    ExtensionLoader(Class<?> type) {
        this.type = type;
    }

    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

    public T getExtension(String name) {
        try {
            if(classes.isEmpty()) {
                loadFile(classes);
            }

            return injectExtension(classes.get(name).newInstance());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void loadFile(Map<String, Class<T>> classes) {
        String fileName = SERVICES_DIRECTORY + type.getName();
        try {
            ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
            Enumeration<URL> urls = classLoader.getResources(fileName);

            if(urls == null) return;
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                Files.readAllLines(Paths.get(url.toURI()), StandardCharsets.UTF_8).forEach((line) -> {
                    Matcher matcher = pattern.matcher(line);
                    if (matcher.find()) {
                        if(logger.isDebugEnabled()) {
                            logger.debug("load [{}:{}]", matcher.group(1), matcher.group(2));
                        }
                        classes.put(matcher.group(1), toClass(matcher.group(2)));
                    }
                });
            }
        } catch (Exception e) {
            throw new RuntimeException("load class exception...cause", e);
        }
    }

    private Class toClass(String className) {
        try {
            Class clazz = Class.forName(className);
            if (!type.isAssignableFrom(clazz)) {
                throw new IllegalStateException("Error when load extension class(interface: " +
                        type + ", class line: " + clazz.getName() + "), class "
                        + clazz.getName() + "is not subtype of interface.");
            }
            return clazz;
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    public T getAdaptiveExtension() {
        Object instance = cachedAdaptiveInstance.get();
        if (instance == null) {
            synchronized (cachedAdaptiveInstance) {
                instance = cachedAdaptiveInstance.get();
                if (instance == null) {
                    try {
                        instance = createAdaptiveExtension();
                        cachedAdaptiveInstance.set(instance);
                    } catch (Throwable t) {
                        throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
                    }
                }
            }
        }
        return (T) instance;
    }

    public T createAdaptiveExtension() {
        try {
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
        } catch (Exception e) {
            throw new IllegalStateException("Can not create adaptive extenstion " + type + ", cause: " + e.getMessage(), e);
        }
    }

    private Class<T> getAdaptiveExtensionClass() {
        ClassPool pool = ClassPool.getDefault();

        CtClass ctClass = pool.makeClass(type.getCanonicalName() + "$Adaptive");
        try {
            ctClass.addInterface(pool.get(type.getCanonicalName()));
            Method[] methods = type.getMethods();

            for(Method method : methods) {
                Class<?> rt = method.getReturnType();
                Class<?>[] pts = method.getParameterTypes();
                Class<?>[] ets = method.getExceptionTypes();
                // e.g. public String hello(String arg0) { return null; }
                StringBuffer methodBuffer = new StringBuffer("public ");
                methodBuffer.append(rt.getCanonicalName()).append(" ");
                methodBuffer.append(" ").append(method.getName()).append("(");
                if(ArrayUtils.isNotEmpty(pts)) {
                    for (int i=0; i<=pts.length-1; i++) {
                        if(i>0) methodBuffer.append(",");
                        methodBuffer.append(pts[i].getName()).append(" ").append("arg"+i);
                    }
                }
                methodBuffer.append(")");
                if(ArrayUtils.isNotEmpty(ets)) {
                    methodBuffer.append(" throws ");
                    for (int i=0; i<=ets.length-1; i++) {
                        if(i>0) methodBuffer.append(",");
                        methodBuffer.append(ets[i].getName());
                    }
                }
                methodBuffer.append("{ ");
                int urlIndex = ArrayUtils.indexOf(pts, com.xz.simplerpc.extension.URL.class);
                if(urlIndex != -1) {
                    methodBuffer.append(String.format("\n%s url = %s;", com.xz.simplerpc.extension.URL.class.getCanonicalName(), "arg"+urlIndex));
                    methodBuffer.append("\nString extName = url.getProtocol();");
                    methodBuffer.append(String.format("\nif(extName==null) throw new IllegalStateException(\"Fail to get extension(%s)\");", type.getCanonicalName()));
                    methodBuffer.append(String.format("\n%s extension = (%s)%s.getExtensionLoader(%s.class).getExtension(extName);",
                            type.getCanonicalName(), type.getCanonicalName(), ExtensionLoader.class.getCanonicalName(), type.getCanonicalName()));
                    if(!void.class.equals(rt.getName())) {
                        methodBuffer.append("\nreturn ");
                    }
                    String paramsStr = new String();
                    if(ArrayUtils.isNotEmpty(pts)) {
                        for (int i=0; i<=pts.length-1; i++) {
                            if(i>0) paramsStr +=",";
                            paramsStr +=("arg"+i);
                        }
                    }
                    methodBuffer.append(String.format("extension.%s(%s);", method.getName(), paramsStr));
                } else {
                    methodBuffer.append("throw new UnsupportedOperationException(\"method ")
                            .append(method.toString()).append(" of interface ")
                            .append(type.getName()).append(" is not adaptive method!\");");
                }
                methodBuffer.append(" }");
                logger.debug(methodBuffer.toString());
                ctClass.addMethod(CtMethod.make(methodBuffer.toString(), ctClass));
            }
            ctClass.writeFile("D:\\git source\\simple-rpc\\dubbo-simplerpc");
            ctClass.defrost();
            return ctClass.toClass();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private T injectExtension(T instance) {
        for (Method method : instance.getClass().getMethods()) {
            if (method.getName().startsWith("set")
                    && method.getParameterTypes().length == 1
                    && Modifier.isPublic(method.getModifiers())) {
                Class<?> pt = method.getParameterTypes()[0];
                try {
                    String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                    Object object = getExtensionLoader(pt).getAdaptiveExtension();
                    if (object != null) {
                        method.invoke(instance, object);
                    }
                } catch (Exception e) {
                    logger.error("fail to inject via method " + method.getName()
                            + " of interface " + type.getName() + ": " + e.getMessage(), e);
                }
            }
        }
        return instance;
    }
}
